<?php
namespace Library\Dal\Driver\Mysql;

/**
 * ActiveRecord
 * 
 * @class Library.Dal.Driver.Mysql.ActiveRecord
 */
class ActiveRecord implements \Library\Dal\ActiveRecordInterface
{
	private $_query;
	
	private $_parameters = array();
	
	private $_type;
	
	/**
	 * If parenthese is 0, nothing will happen, if it is 1 an open parenthese will be included in the next method.
	 * If it is -1, the next method will contain a closing parenthese.
	 * 
	 * @property {Integer} _parenthese
	 * @private
	 */
	private $_parenthese = 0;
	
	/**
	 * Last statement used for the query string. This can be '_where' or '_like' for example.
	 * 
	 * @property {String} _lastStmt
	 * @private
	 */
	private $_lastStmt;
	
	private $_insert;
	private $_select;
	private $_update;
	private $_delete;
	
	private $_from;
	private $_where;
	
	private $_fields;
	private $_values;
	
	private $_set;
	
	private $_orderBy;
	private $_groupBy;
	private $_having;
	private $_limit;
	private $_join;
	
	const INSERT = 'insert';
	const SELECT = 'select';
	const UPDATE = 'update';
	const DELETE = 'delete';
	
	/**
	 * Converts array
	 */
	private function _forceArray($array)
	{
		if (isset($array['0']) && is_array($array['0']))
		{
			$array = $array['0'];
		}
		
		if (!is_array($array))
		{
			$array = array($array);
		}
		
		return $array;
	}
	
	/**
	 * Build query
	 * 
	 * @todo The operaters will be set in the inserquery method itself, not in build query. So that we can avoid using the properties :)
	 * @method _buildQuery
	 * @return {}
	 * @private
	 */
	private function _buildQuery()
	{
		if ($this->_query)
		{
			return $this->_query;
		}		
		
		switch ($this->_type)
		{
			case self::INSERT:
				$this->_query = $this->_buildInsertQuery();
				break;
			case self::SELECT:
				$this->_query = $this->_buildSelectQuery();
				break;
			case self::UPDATE:
				$this->_query = $this->_buildUpdateQuery();
				break;
			case self::DELETE:
				$this->_query = $this->_buildDeleteQuery();
				break;
		}
        
		return $this->_query;
	}
	
	private function _buildInsertQuery()
	{
		$insert = ($this->_insert) 
			? 'INSERT INTO `' . $this->_insert . '` (' . $this->_fields . ') VALUES (' . $this->_values . ');'
			: '';
		
		$query = $insert;
		
		return $query;
	}
	
	private function _buildSelectQuery()
	{
		$select 	= ($this->_select) ? 'SELECT ' . $this->_select	 : '';
		$from    	= ($this->_from) ? ' FROM ' . $this->_from : '';
		$where   	= ($this->_where) ? ' WHERE ' . $this->_where : '';
		$groupBy 	= ($this->_groupBy) ? ' GROUP BY ' . $this->_groupBy : '';
		$having 	= ($this->_having) ? ' HAVING ' . $this->_having : '';
		$orderBy 	= ($this->_orderBy) ? ' ORDER BY ' . $this->_orderBy : '';
		$limit		= ($this->_limit) ? ' LIMIT ' . $this->_limit : '';
		
		$query = $select . $from . $this->_join . $where . $groupBy . $having . $orderBy . $limit;
		
		return $query;
	}
	
	private function _buildUpdateQuery()
	{
		$update 	= ($this->_update) ? 'UPDATE ' . $this->_update : 'UPDATE';
		$set		= ($this->_set) ? ' SET ' . $this->_set : '';
		$where   	= ($this->_where) ? ' WHERE ' . $this->_where : '';
		$orderBy 	= ($this->_orderBy) ? ' ORDER BY ' . $this->_orderBy : ''; 
		$limit		= ($this->_limit) ? ' LIMIT ' . $this->_limit : '';  
		
		return $update . $set . $where . $orderBy . $limit;
	}
	
	private function _buildDeleteQuery()
	{
		$delete     = ($this->_delete) ? 'DELETE ' . $this->_delete : 'DELETE';
		$from    	= ($this->_from) ? ' FROM ' . $this->_from : '';
		$where   	= ($this->_where) ? ' WHERE ' . $this->_where : '';
		$groupBy 	= ($this->_groupBy) ? ' GROUP BY ' . $this->_groupBy : '';
		$having 	= ($this->_having) ? ' HAVING ' . $this->_having : '';
		$orderBy 	= ($this->_orderBy) ? ' ORDER BY ' . $this->_orderBy : '';
		$limit		= ($this->_limit) ? ' LIMIT ' . $this->_limit : '';
		
		$query = $delete . $from . $this->_join . $where . $orderBy . $limit;
		
		return $query;
	}
	
	/**
	 * Set an open parenthese '(' for the next method.
	 * 
	 * @method openParenthese
	 * @return {Library.Dal.Driver.Mysql.ActiveRecord} $this
	 * @return ActiveRecord
	 * @chainable
	 */
	public function openParenthese()
	{
		$this->_parenthese = 1;
		
		return $this;
	}
	
	/**
	 * Set an closing parenthese ')' for the next method.
	 * 
	 * @method closeParenthese
	 * @return {Library.Dal.Driver.Mysql.ActiveRecord} $this
	 * @return ActiveRecord
	 * @chainable
	 */
	public function closeParenthese()
	{
		$this->{$this->_lastStmt} .= ' )';
		
		return $this;
	}
	
	/**
	 * Insert
	 * 
	 * @param {String} $table
	 * @param {Array} $data
	 * @return {Library.Dal.Driver.Mysql.ActiveRecord} $this
	 * @return ActiveRecord
	 * @chainable
	 */
	public function insert($table, $data = null)
	{
		$this->_type = self::INSERT;
		
		$this->_insert = $table;
		
		if ($data !== null && is_array($data))
		{
			foreach ($data as $key => $value)
			{
				$param = $this->_parseParam($key);
				
				$this->bind(':i_' . $param, $value);
				
				$this->_fields .= '`' . $key . '`, ';
				$data[$key] = ':i_' . $param;
			}
			
			$this->_fields = substr($this->_fields, null, -2);
			$this->_values = implode(', ', $data);
		}
		
		return $this;
	}
	
	/**
	 * Select
	 * 
	 * @return {Library.Dal.Driver.Mysql.ActiveRecord} $this
	 * @return ActiveRecord
	 * @chainable
	 */
	public function select($select)
	{
		$this->_type = self::SELECT;
		
		$fields = $this->_forceArray(func_get_args());
				
		foreach ($fields as $field)
		{
			$this->_select .= ($this->_select) ? ', ' . $field : $field;
		}

		$this->_methodEnded(__FUNCTION__, '_select');
				
		return $this;
	}
	
	/**
	 * Update
	 * 
	 * @return {Library.Dal.Driver.Mysql.ActiveRecord} $this
	 * @return ActiveRecord
	 * @chainable
	 */
	public function update($table, array $data = array())
	{
		$this->_type = self::UPDATE;
		
		$this->_update = $table;
		
		foreach ($data as $field => $value)
		{
			$this->set($field, $value);
		}
		
		$this->_methodEnded(__FUNCTION__, '_update');
		
		return $this;
	}
	
	/**
	 * Set a field for an update query
	 * 
	 * @method set
	 * @param {String} $field
	 * @param {String} $value
	 * @return {Library.Dal.Driver.Mysql.ActiveRecord} $this
	 * @return ActiveRecord
	 * @chainable
	 */
	public function set($field, $value)
	{
		$key = ':s_' . $field;
		
		$this->bind($key, $value);
		
		$set = $this->_convertField($field) . ' = ' . $key;
		$this->_set .= ($this->_set) ? ', ' . $set : $set;
		
		$this->_methodEnded(__FUNCTION__, '_set');
		
		return $this;
	}
	
	/**
	 * Delete
	 * 
	 * @return {Library.Dal.Driver.Mysql.ActiveRecord} $this
	 * @return ActiveRecord
	 * @chainable
	 * @todo FROM is handled in this method, check for LOW_PRIORITY etc. in the field names
	 */
	public function delete()
	{
		$this->_type = self::DELETE;
		
		$fields = $this->_forceArray(func_get_args());
		
		$this->_delete .= implode(', ', $fields);
		
		$this->_methodEnded(__FUNCTION__, '_delete');
		
		return $this;
	}
	
	/**
	 * From
	 * 
	 * @return {Library.Dal.Driver.Mysql.ActiveRecord} $this
	 * @return ActiveRecord
	 * @chainable
	 */
	public function from()
	{
		$tables = $this->_forceArray(func_get_args());
		
		foreach ($tables as $table)
		{
			$table = '`' . $table . '`';
    		$this->_from .= ($this->_from) ? ', ' . $table : $table;
		}
		
		$this->_methodEnded(__FUNCTION__, '_from');
				
		return $this;
	}
	
	/**
	 * Handles all the type of where statements. It will prefix the PDO params with 'w_'.
	 * 
	 * @method _where
	 * @param {Array} $where
	 * @param {String} $prefix
	 * @private
	 */
	private function _where($where, $value = null, $prefix = '', $suffix = '')
	{
		// Support for: where('username != ?', 'value');
		if (!is_array($where))
		{
			if ($value !== null)
			{
				$where = array($where => $value);
			}
			else
			{
				$where = array($where);
			}
		}
		
		if ($this->_parenthese === 1)
		{
			$prefix = $prefix . ' (';
		}
		
		end($where);
		$endKey = key($where);
		
		foreach ($where as $stmt => $value)
		{
			$suffix = ($stmt === $endKey) ? '' : ' ' . $suffix;
			
			if(gettype($stmt) === 'integer')
			{
				$stmt = $value;
				$this->_where .= ' ' . $prefix . ' ' . $stmt . $suffix;
			}
			else
			{
				$test = preg_match('/^([a-zA-Z0-9\_\-\.\s]+)[\s]?([\!\=|\=|\<|\>|\>\=|\>\=]+)[\s]?(.*)$/i', $stmt, $matches);
				
				// Ok, the key of the where is a statement with params or something, let's go fix this to real MySQL
				if ($test)
				{
					$field = trim($matches[1]);
					$param = ':w_' . $field;
					
					if (in_array($param, $this->_parameters))
					{
						$param = $param . time();
					}

					$this->bind($param, $value);
					$this->_where .= ' ' . $prefix . ' `' . $field . '` ' . $matches[2] . ' ' . str_replace('?', $param, $matches[3]) . $suffix;
				}
				// Preg match failed, so we think this where is just a key-value pair
				else
				{
					$this->bind(':w_' . $stmt, $value);
					$this->_where .= ' ' . $prefix . ' `' . $stmt . '`' . ' = :w_' . $stmt . $suffix;
				}
			}
		}
		
		$this->_methodEnded(__FUNCTION__, '_where');
	}
	
	/**
	 * Where statement
	 * 
	 * Use this method to begin a WHERE statement. By default it will continue using AND as suffix. So if you will
	 * enter the following:
	 * 
	 * 	$ar->select('*')
	 * 	   ->from('users')
	 * 	   ->where(array(
	 * 			'username' => 'Hotel',
	 * 			'age'	   => 18,
	 * 			'gender'   => 'male'
	 * 		));
	 * 
	 * This will produce the following MySQL string:
	 * 
	 * 	SELECT * FROM `users` WHERE `username` = :w_username AND `age` = :w_age AND `gender` = :w_gender
	 * 
	 * You can also use your own functions and stuff:
	 * 
	 * 	where(array(
	 * 		'age IN (18, 19, 20)' => null 
	 * 	));
	 * 
	 * @method where
	 * @param {Array} $where
	 * @return {Library.Dal.Driver.Mysql.ActiveRecord} $this
	 * @return ActiveRecord
	 * @chainable
	 */
	public function where($where, $value = null)
	{
		$this->_where($where, $value, '', 'AND');
		
		return $this;
	}
	
	/**
	 * Or where statement
	 * 
	 * Use this method if you want to continue your where statement with the OR operator. See the documentation
	 * of the **{@link Library.Dal.Driver.Mysql.ActiveRecord#where}** method for more information.
	 * 
	 * @method orWhere
	 * @param {Array} $where
	 * @param {Array} $where
	 * @return {Library.Dal.Driver.Mysql.ActiveRecord} $this
	 * @return ActiveRecord
	 * @chainable
	 */
	public function orWhere($where, $value = null)
	{
		$this->_where($where, $value, 'OR');
		
		return $this;
	}
	
	/**
	 * And where statement
	 * 
	 * Use this method if you want to continue your where statement with the AND operator. See the documentation
	 * of the **{@link Library.Dal.Driver.Mysql.ActiveRecord#where}** method for more information.
	 * 
	 * @method andWhere
	 * @param {Array} $where
	 * @param {Array} $where
	 * @return {Library.Dal.Driver.Mysql.ActiveRecord} $this
	 * @return ActiveRecord
	 * @chainable
	 */
	public function andWhere($where, $value = null)
	{
		$this->_where($where, $value, 'AND');

		return $this;
	}
	
	/**
	 * Xor where statement
	 * 
	 * Use this method if you want to continue your where statement with the XOR operator. See the documentation
	 * of the **{@link Library.Dal.Driver.Mysql.ActiveRecord#where}** method for more information.
	 * 
	 * @method xorWhere
	 * @param {Array} $where
	 * @param {Array} $where
	 * @return {Library.Dal.Driver.Mysql.ActiveRecord} $this
	 * @return ActiveRecord
	 * @chainable
	 */
	public function xorWhere($where, $value = null)
	{
		$this->_where($where, $value, 'XOR');
		
		return $this;
	}
	
	/**
	 * Order by
	 * 
	 * @method orderBy
	 * @return {Library.Dal.Driver.Mysql.ActiveRecord} $this
	 * @return ActiveRecord
	 * @chainable
	 */
	public function orderBy()
	{
		$fields = $this->_forceArray(func_get_args());
		
		foreach ($fields as $field)
		{
			$field = $this->_convertField($field);
			$this->_orderBy .= ($this->_orderBy) ? ', ' . $field : $field;
		}
		
		$this->_methodEnded(__FUNCTION__, '_orderBy');
		
		return $this;
	}
	
	/**
	 * Group by
	 * 
	 * @method groupBy
	 * @return {Library.Dal.Driver.Mysql.ActiveRecord} $this
	 * @return ActiveRecord
	 * @chainable
	 */
	public function groupBy()
	{
		$fields = $this->_forceArray(func_get_args());
		
		foreach ($fields as $field)
		{
			$field = $this->_convertField($field);
			$this->_groupBy .= ($this->_groupBy) ? ', ' . $field : $field;
		}
		
		$this->_methodEnded(__FUNCTION__, '_groupBy');
		
		return $this;
	}
	
	/**
	 * Convert a string of this format: users.username 
	 * into this: `users`.username
	 * 
	 * @param {String} $field
	 * @return {String}
	 */
	private function _convertField($field)
	{
		$str = strstr($field, '.', true);
		return ($str) ? '`' . $str . '`' . substr($field, strlen($str)) : '`' . $field . '`';
	}
	
	/**
	 * Having
	 * 
	 * @method having
	 * @return {Library.Dal.Driver.Mysql.ActiveRecord} $this
	 * @return ActiveRecord
	 * @chainable
	 */
	public function having($having)
	{
		$this->_having = $having;
		
		$this->_methodEnded(__FUNCTION__, '_having');
		
		return $this;
	}
	
	/**
	 * Limit
	 * 
	 * @method limit
	 * @param {Integer} $offset
	 * @param {Integer} $limit
	 * @return {Library.Dal.Driver.Mysql.ActiveRecord} $this
	 * @return ActiveRecord
	 * @chainable
	 */
	public function limit($limit, $offset = 0)
	{
		$this->_limit = ($offset == 0) ? $limit : $offset . ', ' . $limit;
		
		$this->_methodEnded(__FUNCTION__, '_limit');
		
		return $this;
	}
	
	/**
	 * Join syntax
	 * 
	 * @method _join
	 * @param {String} $type [$type="JOIN"]
	 * @param {String} $table
	 * @param {String} $condition
	 * @private
	 */
	private function _join($type = 'JOIN', $table, $condition)
	{
		$this->_join .= ' ' . $type . ' `' . $table . '` ON (' . $condition . ')';
		
		$this->_methodEnded(__FUNCTION__, '_join');
	}
	
	/**
	 * Regular join
	 * 
	 * @param {String} $table
	 * @param {String} $condition
	 * @return {Library.Dal.Driver.Mysql.ActiveRecord} $this
	 * @return ActiveRecord
	 * @chainable
	 */
	public function join($table, $condition)
	{
		$this->_join('JOIN', $table, $condition);
		
		return $this;
	}
	
	/**
	 * Inner join
	 * 
	 * @param {String} $table
	 * @param {String} $condition
	 * @return {Library.Dal.Driver.Mysql.ActiveRecord} $this
	 * @return ActiveRecord
	 * @chainable
	 */
	public function joinInner($table, $condition)
	{
		$this->_join('INNER JOIN', $table, $condition);
		
		return $this;
	}
	
	/**
	 * Cross join
	 * 
	 * @param {String} $table
	 * @param {String} $condition
	 * @return {Library.Dal.Driver.Mysql.ActiveRecord} $this
	 * @return ActiveRecord
	 * @chainable
	 */
	public function joinCross($table, $condition)
	{
		$this->_join('CROSS JOIN', $table, $condition);
		
		return $this;
	}
	
	/**
	 * Straight join
	 * 
	 * @param {String} $table
	 * @param {String} $condition
	 * @return {Library.Dal.Driver.Mysql.ActiveRecord} $this
	 * @return ActiveRecord
	 * @chainable
	 */
	public function joinStraight($table, $condition)
	{
		$this->_join('STRAIGHT_JOIN', $table, $condition);
		
		return $this;
	}
	
	/**
	 * Left join
	 * 
	 * @param {String} $table
	 * @param {String} $condition
	 * @return {Library.Dal.Driver.Mysql.ActiveRecord} $this
	 * @return ActiveRecord
	 * @chainable
	 */
	public function joinLeft($table, $condition)
	{
		$this->_join('LEFT JOIN', $table, $condition);
		
		return $this;
	}
	
	/**
	 * Right join
	 * 
	 * @param {String} $table
	 * @param {String} $condition
	 * @return {Library.Dal.Driver.Mysql.ActiveRecord} $this
	 * @return ActiveRecord
	 * @chainable
	 */
	public function joinRight($table, $condition)
	{
		$this->_join('RIGHT JOIN', $table, $condition);
		
		return $this;
	}
	
	/**
	 * Left outer join
	 * 
	 * @param {String} $table
	 * @param {String} $condition
	 * @return {Library.Dal.Driver.Mysql.ActiveRecord} $this
	 * @return ActiveRecord
	 * @chainable
	 */
	public function joinLeftOuter($table, $condition)
	{
		$this->_join('LEFT OUTER JOIN', $table, $condition);
		
		return $this;
	}
	
	/**
	 * Right outer join
	 * 
	 * @param {String} $table
	 * @param {String} $condition
	 * @return {Library.Dal.Driver.Mysql.ActiveRecord} $this
	 * @return ActiveRecord
	 * @chainable
	 */
	public function joinRightOuter($table, $condition)
	{
		$this->_join('RIGHT OUTER JOIN', $table, $condition);
		
		return $this;
	}
	
	public function getParameters()
	{
		return $this->_parameters;
	}
	
	/**
	 * Bind paremeter 
	 *
	 * @param {String} $key
	 * @param {String} $value
	 * @param {Library.Dal.DataType::String} $dataType
	 * @return {Library.Dal.Driver.Mysql.ActiveRecord} $this
	 * @return ActiveRecord
	 * @chainable
	 */
	public function bind($key, $value, $dataType = \Library\Dal\DataType::String)
	{
		if ($value === \Library\Dal\DataType::Null)
		{
			$dataType = \Library\Dal\DataType::Null;
			$value = null;
		}
		
		$this->_parameters[] = array($key, $value, $dataType);
		
		return $this;
	}
	
	/**
	 * Called when a ActiveRecord method has ended.
	 * 
	 * @method _methodEnded
	 * @param {String} $method 		Method name which called this method
	 * @param {String} $queryStmt	Query statement name (_where, _like etc.)
	 * @private
	 */
	private function _methodEnded($method, $queryStmt)
	{
		$this->_lastStmt = $queryStmt;
		
		// Set the parenthese to 0 so the next method wont begin with a parenthese
		$this->_parenthese = 0;
	}
	
	private function _parseParam($param)
	{
		$param = str_replace(' ', '_', $param);
		$param = str_replace('-', '_', $param);
		
		return $param;
	}
	
	public function __toString()
	{
		return (string)$this->_buildQuery();
	}
}